<?php

namespace app\middleware;

use think\Request;

/**
 * 注解鉴权
 * 通过反射获取当前请求的控制器类和方法，并解析注解中的指定规则执行鉴权
 * 本中间件会在$request上挂载一个 AuthComment 对象，它是本类的实例
 * 在控制器中可以通过 request()->middleware('AuthComment') 或者 request->AuthComment 来获取本实例
 * 
 * ==== 本中间件支持的注解规则如下:
 * 1. 多个身份鉴权，只要任意一个通过则放行：
 * 		@AuthComment loginModels ["modelA","modelB"]
 * 2. 关闭鉴权
 * 		@AuthComment loginModels []
 * 3. 放行策略，写在权限数组空格后，可选 any（权限数组中任意一个匹配就放行）、all（全部放行），默认为any
 * 		@AuthComment loginModels [] all
 * 4. 在控制器类和方法中都可以使用注解，方法会覆盖类的注解
 * 
 * ==== 在控制器中获取鉴权通过的模型数据
 * 		$admin = request()->AuthComment->getLoginModel('Admin');
 * 
 * 		// 如果需要强制重新获取模型 （例如该接口不限制任何权限模型，但可能会传入某些权限模型）
 * 		$admin = request()->AuthComment->getLoginModelForce('Admin');
 * 
 * ==== 在本类中定义鉴权规则
 * 1. 声明一个方法，方法名为 process + 鉴权实体名，例如 processAdmin
 * 2. 在该方法中返回一个鉴权实体对象，如果鉴权失败则返回null
 */
class AuthComment
{
	protected function processAdmin(Request $request)
	{
		$token = $request->param('TOKEN', '');
		$modelClass = 'app\model\Admin';
		$tokenApp = $modelClass::getTokenApp();
		$admin = $tokenApp->getData($token);
		if ($admin) {
			// 更新token ttl
			$tokenApp->ttl($token, $tokenApp->getConfig()['expire_time']);
		}
		return $admin;
	}

	protected function processUser(Request $request)
	{
		$token = $request->param('TOKEN', '');
		$modelClass = 'app\model\User';
		$tokenApp = $modelClass::getTokenApp();
		$user = $tokenApp->getData($token);
		if ($user) {
			// 更新token ttl
			$tokenApp->ttl($token, $tokenApp->getConfig()['expire_time']);
		}
		return $user;
	}

	/**
	 * 储存要求登陆的模型数据
	 * @var array
	 */
	protected $loginModels = [];
	public function handle($request, \Closure $next)
	{
		// 要求登陆的规则，解析完成后储存到 $this->loginModels 中
		$this->loginModels = [];
		$commentOption = $this->parseAuthComment($request);
		if ($commentOption) {
			$rule = $commentOption['rule']; // 通过规则，all|any
			$models = $commentOption['models'];

			$pass = false;
			if ($rule == 'all' || empty($models)) $pass = true;

			foreach ($models as $modelName) {
				$processMethodName = 'process' . $modelName;
				if (!method_exists($this, $processMethodName)) {
					throw new \Exception('AuthComment 中间件缺少方法: ' . $processMethodName);
				}
				$result = $this->$processMethodName($request);
				$this->loginModels[$modelName] = $result;
				if (!$pass && !!$result) $pass = true;
			}

			if (!$pass) return failed('身份无效，请重新登陆', 401);
		}

		// 挂载本实例到$request
		$request->AuthComment = $this;

		return $next($request);
	}

	/**
	 * 获取模型实例
	 * @param string $modelName 要获取的模型名: Admin, User
	 */
	public function getLoginModel($modelName = null)
	{
		if (is_null($modelName)) {
			return $this->loginModels;
		} else {
			return $this->loginModels[$modelName] ?? null;
		}
	}

	/**
	 * 强制重新获取登录模型
	 * @param string $modelName 模型名，例如：Admin,User
	 * @param Request $request
	 * @return Model|null
	 */
	public function getLoginModelForce($modelName, Request $request)
	{
		$processMethodName = 'process' . $modelName;
		if (!method_exists($this, $processMethodName)) {
			throw new \Exception('AuthComment 中间件缺少方法: ' . $processMethodName);
		}
		$result = $this->$processMethodName($request ?? request());
		return $result;
	}

	/**
	 * 解析出@AuthComment这一行注释
	 * 鉴权模型的含义和优先级规则如下:
	 * 1. 控制器的 function 中的注解会覆盖 class 中的注解
	 * 2. 空数组被认为等同于不设置任何鉴权
	 * 3. 数组中多个模型只要有一个通过，即可通过中间件。其中通过的模型会被挂载到$request上，未通过的会是false
	 * （如果想实现异或关系也可以通过注解来规定一个开关从而实现）
	 */
	protected function parseAuthComment(Request $request): array|false
	{
		$commentLines = '';

		$className = $this->parseControllerClassName($request);
		$class = new \ReflectionClass($className);
		$actionName = $request->action();
		if ($class->hasMethod($actionName)) {
			$action = $class->getMethod($actionName);
			$commentLines = $this->parseComment($action->getDocComment());
			foreach ($commentLines as $commentLine) {
				if ($commentLine[0] === 'AuthComment') {
					return [
						'models' => json_decode($commentLine[2], true),
						'rule' => $commentLine[3] ?? 'some',
					];
				}
			}
		}

		$commentLines = $this->parseComment($class->getDocComment());
		foreach ($commentLines as $commentLine) {
			if ($commentLine[0] === 'AuthComment') {
				return [
					'models' => json_decode($commentLine[2], true),
					'rule' => $commentLine[3] ?? 'some',
				];
			}
		}

		return false;
	}

	/**
	 * 通过Request解析当前控制器类名
	 * @param Request $request
	 * @return string
	 */
	protected function parseControllerClassName(Request $request)
	{
		$namespace = app()->getNamespace();
		$controller = str_replace('.', '\\', $request->controller());
		$classname = $namespace . '\controller\\' . $controller;
		return $classname;
	}

	/**
	 * 将comment解析为
	 * @开头 每行一个元素
	 * @param string $commit 注释全文
	 * @return array
	 */
	protected function parseComment(string $comment)
	{
		$line_array = preg_split('/\r\n|\r|\n/', $comment);
		$result = [];
		foreach ($line_array as $line) {
			// 过滤无@开头的项
			if (strpos($line, '@') === false) continue;

			// 移除@之前的符号 [/ * @title 标题] ===> [title 标题]
			$res = preg_replace("/.+@/", '', $line);

			// 除去多个空格的情况 [param int id  商品ID] ===> [param int id 商品ID]
			$res = preg_replace("/\s+/", ' ', $res);

			// 按空格分隔 ['param', 'int', 'id', '商品ID'], 
			$result[] = explode(' ', $res);
		}
		return $result;
	}
}
